精通Linux系列三十五:使用Shell脚本编程
点击关注公众号,AI&编程干货及时送达
使用Shell脚本编程
之前当我们讲到shell(即bash)时,我们提到它内置了一个编程语言。事实上,你可以编写程序或者称之为shell脚本,来完成单一命令无法完成的任务。命令reset-lpg
,在书中的示例目录中提供,是一个你可以阅读的shell脚本:
→ less ~/linuxpocketguide/reset-lpg
像任何好的编程语言,shell有变量、条件语句(如if-then-else)、循环、输入和输出等功能。关于shell脚本编程,已经有很多书籍。因此,我们仅提供最基础的内容帮助你入门。要查看完整文档,运行info bash
,或者在网上搜索,或者查阅更深入的O’Reilly书籍,如《学习bash Shell》或《Bash口袋参考手册》。
创建和运行Shell脚本
要创建一个shell脚本,只需将bash命令放入一个文件中,就像你直接输入它们那样。要运行脚本,你有三个选择:
• 在文件前添加
#!/bin/bash
并使文件可执行这是运行脚本的最常见方法。在脚本文件的顶部添加行:#!/bin/bash
。它必须是文件的第一行,左对齐。然后使文件可执行:→ **
chmod +x myscript**
。可以选择将其移动到搜索路径中的一个目录。然后像运行其他命令那样运行它:→ **
myscript**
。如果脚本位于你当前的目录,但当前目录"."不在你的搜索路径中,你需要加上"./",这样shell才能找到脚本:→ **
./myscript**
。出于安全原因,当前目录通常不在你的搜索路径中。(你不会希望一个本地脚本(例如)“ls”覆盖真正的ls
命令。)• 传递给bashbash会将其参数解释为脚本的名称并运行它。
→ **
bash myscript**
• 使用“.”或
source
在当前shell中运行上述方法将你的脚本作为一个独立的实体运行,它不会影响到你当前的shell[24]。如果你希望你的脚本能够修改当前的shell(设置变量、更改目录等),可以使用source
或“.”命令在当前shell中运行它:→ **
. myscript** → **
source myscript**
空白和换行
Bash shell脚本对空格和换行非常敏感。因为这个编程语言的“关键词”实际上是由shell解析的命令,所以你需要用空格分隔参数。同样,命令中的换行会误导shell,使其认为命令是不完整的。按照我们在这里呈现的约定,你应该没问题。
如果你必须将一个长命令分成多行,则每行(除最后一行外)都以单个\
字符结束,意味着“继续下一行”:
→ grep abcdefghijklmnopqrstuvwxyz file1 file2 \
file3 file4
变量
我们之前描述过shell变量:
→ MYVAR=6
→ echo $MYVAR
6
所有保存在变量中的值都是字符串,但如果它们是数字,当需要时shell会将它们视为数字:
→ NUMBER="10"
→ expr $NUMBER + 5
15
在shell脚本中引用变量的值时,最好用双引号包围它,以防止某些运行时错误。未定义的变量,或其值中带有空格的变量,如果不用引号包围,会产生意外的值,导致脚本出错:
→ FILENAME="我的文档" 名称中有空格
→ ls $FILENAME 尝试列出它
ls: 我的: 没有这样的文件或目录 ls看到2个参数
ls: 文档: 没有这样的文件或目录
→ ls -l "$FILENAME" 正确地列出它
我的文档 ls只看到1个参数
如果一个变量名与另一个字符串相邻评估,为了防止意外的行为,最好用大括号包围它:
→ HAT="fedora(原文)"
→ echo "The plural of $HAT is $HATs"
fedora的复数是 没有"HATs"这个变量
→ echo "The plural of $HAT is ${HAT}s"
fedora的复数是fedoras 这是我们想要的
输入与输出
脚本输出由echo
和printf
命令提供,我们在[“屏幕输出”]中描述过:
→ echo "Hello world"
你好,世界
→ printf "I am %d years old\n" `expr 20 + 20`
我今年40岁了
输入是由read(原文)
命令提供的,它从标准输入读取一行并存储在一个变量中:
→ read name
Sandy Smith <回车>
→ echo "我读到的名字是 $name"
我读到的名字是 Sandy Smith
布尔值和返回码
在我们描述条件语句和循环之前,我们需要解释布尔(真/假)测试的概念。对于shell,值0表示真或成功,其他任何值都表示假或失败。(可以把零看作“没有错误”,其他值看作错误码。)
此外,每个Linux命令在退出时都会返回一个整数值,称为返回码或退出状态,给shell。
您可以在特殊变量$?
中看到这个值:
→ cat myfile
我的名字是 Sandy Smith,我真的很喜欢 Ubuntu Linux(原文)
→ grep Smith myfile
我的名字是 Sandy Smith 找到了匹配项...
→ echo $?
0 ...所以返回码是“成功”
→ grep aardvark myfile
→ echo $? 没有找到匹配项...
1 ...所以返回码是“失败”
命令的返回码通常在其manpage上有文档记录。
test 和 “[”
test
命令(内置于shell)将评估涉及数字和字符串的简单布尔表达式,并将其退出状态设置为0 (真) 或 1 (假):
→ test 10 -lt 5 10小于5吗?
→ echo $?
1 不,不是
→ test -n "hello" “hello”长度非零吗?
→ echo $?
0 是的,是非零的
以下是用于检查整数、字符串和文件属性的常见test
(test)参数:
文件测试 | |
-d name | 文件 name 是一个目录 |
-f name | 文件 name 是一个普通文件 |
-L name | 文件 name 是一个符号链接 |
-r name | 文件 name 存在且可读 |
-w name | 文件 name 存在且可写 |
-x name | 文件 name 存在且可执行 |
-s name | 文件 name 存在且其大小非零 |
f1 -nt f2 | 文件 f1 比文件 f2 新 |
f1 -ot f2 | 文件 f1 比文件 f2 旧 |
字符串测试 | |
s1 = s2 | 字符串 s1 等于字符串 s2 |
s1 != s2 | 字符串 s1 不等于字符串 s2 |
-z s1 | 字符串 s1 长度为零 |
-n s1 | 字符串 s1 长度非零 |
数字测试 | |
a -eq b | 整数 a 和 b 相等 |
a -ne b | 整数 a 和 b 不相等 |
a -gt b | 整数 a 大于整数 b |
a -ge b | 整数 a 大于或等于整数 b |
a -lt b | 整数 a 小于整数 b |
a -le b | 整数 a 小于或等于整数 b |
组合和否定测试 | |
t1 -a t2 | 与: 测试 t1 和 t2 都为真 |
t1 -o t2 | 或: 测试 t1 或 t2 是真的 |
! your_test | 否定测试 (即,your_test (your_test)为假) |
\( your_test \) | 括号用于分组,如同代数 |
test
(test)有一个不寻常的别名,“[
” (左方括号),用作与条件和循环的简写。如果你使用这个简写,你必须提供一个最终的参数“]
” (右方括号)来标示测试的结束。以下测试与之前的两个相同:
→ [ 10 -lt 5 ]
→ echo $?
1
→ [ -n "hello" ]
→ echo $?
0
请记住,“[
”就像其他命令一样,因此其后跟的是由空格分隔的单独参数。所以如果你误忘了一些空格:
→ [ 5 -lt 4] 4和]之间没有空格
bash: [: missing ']'
那么test
(test)会认为最后的参数是字符串“4]”并抱怨最后的括号丢失了。
一个更强大但不那么便携的布尔测试语法是双括号,[[
,它增加了正则表达式匹配并消除了test
的一些怪癖。详细内容请参见http://mywiki.wooledge.org/BashFAQ/031。
条件语句
if
语句用于选择不同的选项,每个选项可能有复杂的测试条件。最简单的形式是if-then
语句:
if 命令 如果命令的退出状态为0
then
主体
fi
这里有一个带有if
语句的示例脚本:
→ cat 脚本-if(英文:script-if)
#!/bin/bash
if [ `whoami` = "root" ]
then
echo "你是超级用户"
fi
接下来是if-then-else
语句:
if 命令
then
主体1
else
主体2
fi
例如:
→ cat 脚本-else(英文:script-else)
#!/bin/bash
if [ `whoami` = "root" ]
then
echo "你是超级用户"
else
echo "你只是个普通人"
fi
→ ./脚本-else(英文:script-else)
你只是个普通人
→ sudo ./脚本-else(英文:script-else)
密码:********
你是超级用户
最后,我们有if-then-elif-else
形式,你可以有任意多的测试:
if 命令1
then
主体1
elif 命令2
then
主体2
elif ...
...
else
主体N
fi
例如:
→ cat 脚本-elif(英文:script-elif)
#!/bin/bash
bribe=20000
if [ `whoami` = "root" ]
then
echo "你是超级用户"
elif [ "$USER" = "root" ]
then
echo "你可能是超级用户"
elif [ "$bribe" -gt 10000 ]
then
echo "你可以付费成为超级用户"
else
echo "你还只是个普通人"
fi
→ ./脚本-elif(英文:script-elif)
你可以付费成为超级用户
case
语句评估一个单一的值并转到适当的代码片段:
→ cat 脚本-case(英文:script-case)
#!/bin/bash
echo -n "你想做什么(吃东西, 睡觉)?"
read answer
case "$answer" in
eat)
echo "好的,来个汉堡。"
;;
sleep)
echo "那么,晚安。"
;;
*)
echo "我不确定你想做什么。"
echo "我想我们明天再见。"
;;
esac
→ ./脚本-case(英文:script-case)
你想做什么(吃东西, 睡觉)?睡觉
那么,晚安。
一般的形式是:
case 字符串 in
表达式1)
主体1
;;
表达式2)
主体2
;;
...
表达式N)
主体N
;;
*)
其他主体
;;
esac
其中*字符串
是任何值,通常是一个变量值,如$myvar
,表达式1
到表达式N
*都是模式(运行info bash
命令以获取详情),最后的*
类似于最终的"else"。每组命令必须由;;
终止(如下所示):
→ cat 脚本-case2(英文:script-case2)
#!/bin/bash
echo -n "输入一个字母:"
read letter
case $letter in
X)
echo "$letter 是 X"
;;
[aeiou])
echo "$letter 是个元音"
;;
[0-9])
echo "$letter 是个数字,真傻"
;;
*)
echo "字母'$letter' 不被支持"
;;
esac
→ ./脚本-case2(英文:script-case2)
输入一个字母:e
e 是个元音
循环
while
循环会在某个条件为真的情况下重复一组命令。
while 命令 当命令的退出状态为0时
do
主体
done
例如:
→ cat script-while
#!/bin/bash
i=0
while [ $i -lt 3 ]
do
echo "$i"
i=`expr $i + 1`
done
→ ./script-while
0
1
2
until
循环会重复直到某个条件变为真:
until 命令 当命令的退出状态为非零时
do
主体
done
例如:
→ cat script-until
#!/bin/bash
i=0
until [ $i -ge 3 ]
do
echo "$i"
i=`expr $i + 1`
done
→ ./script-until
0
1
2
小心避免无限循环,使用while
的条件始终评估为0(真),或until
的条件始终评估为非零值(假):
i=1
while [ $i -lt 10 ] 变量i永远不会改变。这是无限的!
do
echo "forever"
done
另一种类型的循环,for
循环,遍历来自列表的值:
for 变量 in 列表
do
主体
done
例如:
→ cat script-for
#!/bin/bash
for name in Tom Jane Harry
do
echo "$name是我的朋友"
done
→ ./script-for
Tom是我的朋友
Jane是我的朋友
Harry是我的朋友
for
循环特别适用于处理文件列表;例如,当前目录中具有某个扩展名的文件名:
→ cat script-for2
#!/bin/bash
for file in *.docx
do
echo "$file是一个臭名昭著的Microsoft Word文件(Microsoft Word file)"
done
→ ./script-for2
letter.docx是一个臭名昭著的Microsoft Word文件
您还可以使用seq
命令(查看[seq
])产生一系列连续的整数,然后遍历这些数字:
→ cat script-seq
#!/bin/bash
for i in $(seq 1 20) 生成数字1 2 3 4 ... 20
do
echo "迭代$i"
done
→ ./script-seq
迭代1
迭代2
迭代3
...
迭代20
命令行参数
Shell脚本可以像其他Linux命令一样接受命令行参数和选项。(实际上,一些常见的Linux命令就是脚本。)在您的shell脚本中,您可以将这些参数引用为$1
、$2
、$3
等:
→ cat script-args
#!/bin/bash
echo "我的名字是$1,我来自$2"
→ ./script-args Johnson Wisconsin
我的名字是Johnson,我来自Wisconsin
→ ./script-args Bob
我的名字是Bob,我来自
您的脚本可以使用$#
测试它接收的参数数量:
→ cat script-args2
#!/bin/bash
if [ $# -lt 2 ]
then
echo "$0错误:您必须提供两个参数"
else
echo "我的名字是$1,我来自$2"
fi
特殊值$0
包含脚本的名称,对于使用和错误消息很有用:
→ ./script-args2 Barbara
./script-args2错误:您必须提供两个参数
要遍历所有命令行参数,请使用for
循环和特殊变量$@
,它包含所有参数:
→ cat script-args3
#!/bin/bash
for arg in $@
do
echo "我找到了参数$arg"
done
→ ./script-args3 One Two Three
我找到了参数One
我找到了参数Two
我找到了参数Three
使用返回码退出
exit
命令终止您的脚本并将给定的返回码传递给shell。按照传统,脚本应该返回0表示成功,返回1(或其他非零值)表示失败。如果您的脚本没有调用exit
,返回码自动为0:
→ cat script-exit
#!/bin/bash
if [ $# -lt 2 ]
then
echo "$0错误:您必须提供两个参数"
exit 1
else
echo "我的名字是$1,我来自$2"
fi
exit 0
→ ./script-exit Bob
./script-exit错误:您必须提供两个参数
→ echo $?
1
传输给bash
Bash不仅仅是一个shell;它也是一个命令,bash
,它从标准输入中读取。这意味着你可以构建命令作为字符串,并将它们发送给bash执行:
→ echo wc -l myfile
wc -l myfile
→ echo wc -l myfile | bash
18 myfile
BASH警告
将命令传输到bash非常强大,但也可能非常危险。首先确保你确切地知道哪些命令将被执行。你不希望意外地把rm
命令传输到bash并删除一个有价值的文件(或1,000个有价值的文件)。
如果有人让你检索一个网页(例如,使用curl
命令)并盲目地传输到bash,请不要这样做!相反,将网页作为一个文件捕获(使用curl
或wget
),仔细检查它,然后做出明智的决定是否使用bash执行它。
这种技术非常有用。假设你想从一个网站下载文件photo1.jpg、photo2.jpg,直到photo100.jpg。而不是手动输入100个wget
命令,用循环构建命令,使用seq
构建从1到100的整数列表:
→ for i in `seq 1 100`
do
echo wget http://example.com/photo$i.jpg
done
wget http://example.com/photo1.jpg
wget http://example.com/photo2.jpg
...
wget http://example.com/photo100.jpg
是的,你已经构建了100条命令的文本。现在把输出传输给bash
,它会运行所有100条命令,就像你手动输入它们一样:
→ for i in `seq 1 100`
do
echo wget http://example.com/photo$i.jpg
done | bash
这里有一个更复杂但实用的应用。假设你有一组文件想要重命名。将旧名称放入文件oldnames中,新名称放入newnames文件中:
→ cat oldnames
oldname1
oldname2
oldname3
→ cat newnames
newname1
newname2
newname3
现在使用paste
和sed
命令(“文件文本操作”)将旧的和新的名称并排放置,并在每一行前加上"mv",输出结果是一系列的“mv”命令:
→ cat oldnames | paste -d' ' oldnames newnames \
| sed 's/^/mv /'
mv oldfile1 newfile1
mv oldfile2 newfile2
mv oldfile3 newfile3
最后,将输出传输给bash
,重命名就发生了!
→ cat oldnames | paste -d' ' oldnames newnames \
| sed 's/^/mv /' \
| bash
拓展
Shell脚本对许多目的来说都很好,但Linux还配备了更强大的脚本语言,以及编译型编程语言。以下是其中的一些:
语言 | 程序 | 入门方法... |
C, C++ | gcc , g++ | man gcc [https://gcc.gnu.org/] |
.NET | mono | man mono [http://www.mono-project.com/] |
Java | javac | [http://java.com/] |
Perl | perl | man perl [http://www.perl.com/] |
PHP | php | man php [http://php.net/] |
Python | python | man python [https://www.python.org/] |
Ruby | ruby | [http://www.ruby-lang.org/] |
推荐阅读
你好,我是拾叁,7年开发老司机、互联网两年外企5年。怼得过阿三老美,也被PR comments搞崩溃过。这些年我打过工,创过业,接过私活,也混过upwork。赚过钱也亏过钱。一路过来,给我最深的感受就是不管学什么,一定要不断学习。只要你能坚持下来,就很容易实现弯道超车!所以,不要问我现在干什么是否来得及。如果你还没什么方向,可以先关注我,这里会经常分享一些前沿资讯和编程知识,帮你积累弯道超车的资本。